
//
// Copyright (c) 2008, the /*jupiter jazz*/ group
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//     * Neither the name of the /*jupiter jazz*/ group nor the
//       names of its contributors may be used to endorse or promote products
//       derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY the /*jupiter jazz*/ group ''AS IS'' AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL the /*jupiter jazz*/ group BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//

#ifndef CLASSLIB_COMMANDLINEPARSER_H
#define CLASSLIB_COMMANDLINEPARSER_H

// classlib headers.
#include "classlib/concepts.h"
#include "classlib/exception.h"
#include "classlib/stlext.h"

// Standard headers.
#include <cassert>
#include <cstdarg>
#include <cstddef>
#include <cstdio>
#include <iostream>
#include <limits>
#include <sstream>
#include <string>
#include <vector>

namespace foundation {
namespace commandline {

//
// A vector of strings.
//

typedef std::vector<std::string> StringVector;


//
// String <-> value conversion functions implementation.
//

// Exception thrown by the utility function foundation::from_string()
// when an error occurred when converting a value to a string.
struct ExceptionStringConversionError : public Exception {};

// Convert a value to a string.
template <typename T>
std::string to_string(const T& val)
{
    std::stringstream sstr;
    sstr << val;
    return sstr.str();
}

// Convert a string to a value.
template <typename T>
T from_string(const std::string& s)
{
    std::stringstream sstr;
    sstr << s;

    T val;
    sstr >> val;

    if (sstr.fail())
        throw ExceptionStringConversionError();

    return val;
}
template <>
inline std::string from_string(const std::string& s)
{
    return s;
}
template <>
inline bool from_string(const std::string& s)
{
    if (s == "1" || s == "true" || s == "on" || s == "yes")
        return true;
    else if (s == "0" || s == "false" || s == "off" || s == "no")
        return false;
    else throw ExceptionStringConversionError();
}


//
// Message list.
//

class MessageList
  : public NonCopyable
{
  public:
    // Add a message to the list.
    void add(const char* format, ...);

    // Print the messages.
    void print() const;

  private:
    struct Message
    {
        std::string m_text;
    };

    typedef std::vector<Message> MessageVector;

    MessageVector m_messages;
};


//
// Base class for option handlers.
//

class OptionHandler
  : public NonCopyable
{
  public:
    // Flags.
    enum Flags
    {
        None        = 0,
        Hidden      = 1 << 0,   // don't list this option in program usage
        Repeatable  = 1 << 1    // this option can appear multiple times on a command line
    };

    // Constructor.
    OptionHandler();

    // Add a name for this option.
    void add_name(const std::string& name);

    // Set a description of this option.
    void set_description(const std::string& description);

    // Set the syntax for this option.
    void set_syntax(const std::string& syntax);

    // Set the flags for this option.
    void set_flags(const Flags flags);

    // Return true if this option was found on the command line.
    bool found() const;

  protected:
    friend class Parser;

    StringVector    m_names;
    std::string     m_description;
    std::string     m_syntax;
    Flags           m_flags;
    bool            m_found;

    // Return true if an argument matches any of the name of this option.
    bool match_name(const std::string& arg) const;

    // Return the maximum number of values this option can handle.
    virtual size_t get_max_value_count() const = 0;

    // Parse a vector of values.
    virtual void parse(
        const std::string&      name,
        const StringVector&     values,
        MessageList&            messages) = 0;

    // Print this option to a string.
    virtual void print(std::string& s) const = 0;
};


//
// Flag option handler.
//

class FlagOptionHandler
  : public OptionHandler
{
  private:
    // Return the maximum number of values this option can handle.
    virtual size_t get_max_value_count() const;

    // Parse a vector of values.
    virtual void parse(
        const std::string&      name,
        const StringVector&     values,
        MessageList&            messages);

    // Print this option to a string.
    virtual void print(std::string& s) const;
};


//
// Handler for an option with one or multiple values.
//

template <typename T>
class ValueOptionHandler
  : public OptionHandler
{
  public:
    // Value and value vector types.
    typedef T ValueType;
    typedef std::vector<ValueType> ValueVectorType;

    // Constructor.
    ValueOptionHandler();

    // Set minimum/maximum number of values.
    void set_min_value_count(const size_t min_count);
    void set_max_value_count(const size_t max_count);

    // Return the string values of this option.
    const StringVector& string_values() const;

    // Return the values of this option.
    const ValueVectorType& values() const;

  private:
    size_t              m_min_value_count;
    size_t              m_max_value_count;

    StringVector        m_string_values;
    ValueVectorType     m_values;

    // Return the maximum number of values this option can handle.
    virtual size_t get_max_value_count() const;

    // Parse a vector of values.
    virtual void parse(
        const std::string&  name,
        const StringVector& vals,
        MessageList&        messages);

    // Print this option to a string.
    virtual void print(std::string& s) const;
};


//
// Command line parser.
//

class Parser
  : public NonCopyable
{
  public:
    // Constructor.
    Parser();
    
    // Add an option handler.
    void add_option_handler(OptionHandler* handler);

    // Set a default option handler.
    void set_default_option_handler(OptionHandler* handler);

    // Print the program usage.
    void print_usage() const;

    // Parse a command line.
    void parse(
        const int       argc,
        const char*     argv[]);

    // Print the messages that were generated during parsing.
    void print_messages();

  private:
    struct Option
    {
        std::string     m_name;             // option name, as found on the command line
        OptionHandler*  m_handler;          // option handler
        StringVector    m_values;           // option values
    };

    typedef std::vector<OptionHandler*> OptionHandlerVector;
    typedef std::vector<Option> OptionVector;

    OptionHandlerVector m_handlers;         // option handlers
    OptionVector        m_options;          // options
    Option              m_default_option;   // default option
    MessageList         m_messages;         // messages generated during parsing

    // Return the length of the longest header string.
    size_t get_max_header_length() const;

    // Find an option handler that accepts a given argument.
    OptionHandler* find_option_handler(const std::string& arg) const;

    // Collect the options from a command line.
    void collect_options(
        const int       argc,
        const char*     argv[]);

    // Process the collected options.
    void process_options();
};


//
// Message list.
//

// Add a message to the list.
inline void MessageList::add(const char* format, ...)
{
    Message msg;

    // Size in bytes of the temporary message buffer.
    static const size_t BufferSize = 4096;

    // Print the formatted message into msg.
    va_list argptr;
    va_start(argptr, format);
    char buffer[BufferSize];
    vsnprintf(buffer, BufferSize - 1, format, argptr);
    msg.m_text = buffer;

    // Append the message to the list of messages.
    m_messages.push_back(msg);
}

// Print the messages.
inline void MessageList::print() const
{
    for (const_each<MessageVector> i = m_messages; i; ++i)
        std::cerr << i->m_text;
}


//
// OptionHandler class implementation.
//

// Constructor.
inline OptionHandler::OptionHandler()
  : m_flags(None)
  , m_found(false)
{
}

// Add a name for this option.
inline void OptionHandler::add_name(const std::string& name)
{
    m_names.push_back(name);
}

// Set a description of this option.
inline void OptionHandler::set_description(const std::string& description)
{
    m_description = description;
}

// Set the syntax for this option.
inline void OptionHandler::set_syntax(const std::string& syntax)
{
    m_syntax = syntax;
}

// Set the flags for this option.
inline void OptionHandler::set_flags(const Flags flags)
{
    m_flags = flags;
}

// Return true if this option was found on the command line.
inline bool OptionHandler::found() const
{
    return m_found;
}

// Return true if an argument matches any of the name of this option.
inline bool OptionHandler::match_name(const std::string& arg) const
{
    for (const_each<StringVector> i = m_names; i; ++i)
    {
        if (arg == *i)
            return true;
    }
    return false;
}


//
// FlagOptionHandler class implementation.
//

// Return the maximum number of values this option can handle.
inline size_t FlagOptionHandler::get_max_value_count() const
{
    return 0;
}

// Parse a vector of values.
inline void FlagOptionHandler::parse(
    const std::string&  name,
    const StringVector& values,
    MessageList&        messages)
{
    assert(values.empty());

    if (m_found && !(m_flags & OptionHandler::Repeatable))
    {
        // Error: option already specified.
        messages.add(
            "error: option '%s' already specified.\n",
            name.c_str());
        return;
    }

    // The option was successfully parsed.
    m_found = true;
}

// Print this option to a string.
inline void FlagOptionHandler::print(std::string& s) const
{
    // Print the first name of the option.
    s += m_names.front();
}


//
// ValueOptionHandler class implementation.
//

// Constructor.
template <typename T>
inline ValueOptionHandler<T>::ValueOptionHandler()
  : m_min_value_count(0)
  , m_max_value_count(std::numeric_limits<size_t>::max())
{
}

// Set minimum/maximum number of values.
template <typename T>
inline void ValueOptionHandler<T>::set_min_value_count(const size_t min_count)
{
    m_min_value_count = min_count;
}
template <typename T>
inline void ValueOptionHandler<T>::set_max_value_count(const size_t max_count)
{
    m_max_value_count = max_count;
}

// Return the string values of this option.
template <typename T>
inline const StringVector& ValueOptionHandler<T>::string_values() const
{
    return m_string_values;
}

// Return the values of this option.
template <typename T>
inline const std::vector<T>& ValueOptionHandler<T>::values() const
{
    return m_values;
}

// Return the maximum number of values this option can handle.
template <typename T>
inline size_t ValueOptionHandler<T>::get_max_value_count() const
{
    return m_max_value_count;
}

// Parse a vector of values.
template <typename T>
inline void ValueOptionHandler<T>::parse(
    const std::string&  name,
    const StringVector& vals,
    MessageList&        messages)
{
    assert(vals.size() <= m_max_value_count);

    if (m_found && !(m_flags & OptionHandler::Repeatable))
    {
        // Error: option already specified.
        messages.add(
            "error: option '%s' already specified, ignoring this definition.\n",
            name.c_str());
        return;
    }

    if (vals.size() < m_min_value_count)
    {
        // Error: wrong number of option values.
        std::string error = "option '" + name + "': wrong number of values.";
        if (!m_syntax.empty())
            error += " syntax: " + name + " " + m_syntax + ".";
        messages.add("error: %s\n", error.c_str());
        return;
    }

    // Parse option values.
    for (const_each<StringVector> i = vals; i; ++i)
    {
        try
        {
            m_values.push_back(from_string<T>(*i));
        }
        catch (const ExceptionStringConversionError&)
        {
            // Error: value type mismatch.
            std::string error = "option '" + name + "': type mismatch.";
            if (!m_syntax.empty())
                error += " syntax: " + name + " " + m_syntax + ".";
            messages.add("error: %s\n", error.c_str());
            return;
        }
    }

    // Copy string values locally.
    m_string_values = vals;

    // The option was successfully parsed.
    m_found = true;
}

// Print this option to a string.
template <typename T>
inline void ValueOptionHandler<T>::print(std::string& s) const
{
    // Print the first name of the option.
    s += m_names.front() + " ";

    // Print the values of the option.
    for (const_each<ValueVectorType> i = m_values; i; ++i)
    {
        if (i > m_values.begin())
            s += " ";

        // Print this value.
        s += to_string(*i);
    }
}


//
// Parser class implementation.
//

// Constructor.
inline Parser::Parser()
{
    // No default option handler.
    m_default_option.m_handler = 0;
}

// Add an option handler.
inline void Parser::add_option_handler(OptionHandler* handler)
{
    assert(handler);
    m_handlers.push_back(handler);
}

// Set a default option handler.
inline void Parser::set_default_option_handler(OptionHandler* handler)
{
    assert(handler);
    m_default_option.m_handler = handler;
}

// Print the program usage.
inline void Parser::print_usage() const
{
    const size_t max_header_length = get_max_header_length();

    // Loop over the option handlers.
    for (const_each<OptionHandlerVector> i = m_handlers; i; ++i)
    {
        // Fetch the option handler.
        const OptionHandler* handler = *i;

        // Skip hidden options.
        if (handler->m_flags & OptionHandler::Hidden)
            continue;

        // Build the header string for this option.
        std::string header;
        for (const_each<StringVector> j = handler->m_names; j; ++j)
        {
            if (j > handler->m_names.begin())
                header += ", ";
            header += *j;
        }

        // Pad with spaces so that descriptions are aligned.
        if (max_header_length > header.size())
            header += std::string(max_header_length - header.size(), ' ');

        // Emit the line.
        std::cerr << "  " << header << "  " << handler->m_description << std::endl;
    }
}

// Parse a command line.
inline void Parser::parse(
    const int       argc,
    const char*     argv[])
{
    // Collect the options from a command line.
    collect_options(argc, argv);

    // Process the collected options.
    process_options();
}

// Print the messages that were generated during parsing.
inline void Parser::print_messages()
{
    m_messages.print();
}

// Return the length of the longest header string.
inline size_t Parser::get_max_header_length() const
{
    size_t max_header_length = 0;

    // Loop over the option handlers.
    for (const_each<OptionHandlerVector> i = m_handlers; i; ++i)
    {
        // Fetch the option handler.
        const OptionHandler* handler = *i;

        // Skip hidden options.
        if (handler->m_flags & OptionHandler::Hidden)
            continue;

        // Compute the length of the header string for this option.
        size_t header_length = 0;
        for (const_each<StringVector> j = handler->m_names; j; ++j)
        {
            if (j > handler->m_names.begin())
                header_length += 2;                 // account for the separators
            header_length += j->size();
        }

        // Keep track of the longest header string.
        if (max_header_length < header_length)
            max_header_length = header_length;
    }

    return max_header_length;
}

// Find an option handler that accepts a given argument.
inline OptionHandler* Parser::find_option_handler(const std::string& arg) const
{
    for (const_each<OptionHandlerVector> i = m_handlers; i; ++i)
    {
        // Fetch the handler.
        OptionHandler* handler = *i;

        // Return this handler if one of its name matches the argument.
        if (handler->match_name(arg))
            return handler;
    }

    // No handler found for this argument.
    return 0;
}

// Collect the options from a command line.
inline void Parser::collect_options(
    const int       argc,
    const char*     argv[])
{
    assert(m_options.empty());

    for (int i = 1; i < argc; ++i)
    {
        // Fetch the command line argument.
        const std::string arg = argv[i];

        // Find an option handler that accepts this argument.
        OptionHandler* handler = find_option_handler(arg);

        if (handler)
        {
            // Create a new option.
            Option option;
            option.m_name = arg;
            option.m_handler = handler;
            m_options.push_back(option);
        }
        else
        {
            if (m_options.empty() ||
                m_options.back().m_values.size() >= m_options.back().m_handler->get_max_value_count())
            {
                if (m_default_option.m_handler == 0 ||
                    m_default_option.m_values.size() >= m_default_option.m_handler->get_max_value_count())
                {
                    // Error: unknown option.
                    m_messages.add("error: unknown option '%s'.\n", arg.c_str());
                }
                else
                {
                    // Append this value to the default option.
                    m_default_option.m_values.push_back(arg);
                }
            }
            else
            {
                // Append this value to the current option.
                m_options.back().m_values.push_back(arg);
            }
        }
    }
}

// Process the collected options.
inline void Parser::process_options()
{
    for (const_each<OptionVector> i = m_options; i; ++i)
    {
        // Fetch the option.
        const Option& option = *i;

        // Let the option handler parse the option values.
        option.m_handler->parse(
            option.m_name,
            option.m_values,
            m_messages);
    }

    // Parse the default option values.
    if (m_default_option.m_handler)
    {
        m_default_option.m_handler->parse(
            m_default_option.m_name,
            m_default_option.m_values,
            m_messages);
    }
}

}       // namespace commandline
}       // namespace foundation

#endif  // !CLASSLIB_COMMANDLINEPARSER_H
